package org.abettor.util.upnp;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.abettor.util.StringTool;

import net.sbbi.upnp.impls.InternetGatewayDevice;
import net.sbbi.upnp.messages.UPNPResponseException;

/**
 * 为内网主机提供以NAT方式打开或关闭端口的功能
 * @author <a href="http://www.abettor.org/" target="_blank">abettor</a>
 *
 */
public class NatHelper
{
	private static NatHelper instance;
	private static Object locker = new Object();
	private InternetGatewayDevice[] igds = null;
	private int discoveryTimeout = 10000;
	private List<NatInfo> natInfoList = Collections.synchronizedList(new ArrayList<NatInfo>());

	/**
	 * 取得单例模式对象实例，同时启动一个守护线程，负责过期映射到清理工作
	 */
	public static NatHelper getInstance()
	{
		synchronized(locker)
		{
			if(instance == null)
			{
				instance = new NatHelper();
			}

			return instance;
		}
	}

	//	禁止实例化
	private NatHelper()
	{
		//	TODO 启动一个守护线程，负责过期映射到清理工作
	}

	/**
	 * 获取网关信息
	 * @param discoveryTimeout 超时时间，单位：毫秒。如果为0，则使用默认超时时间（5000毫秒）。
	 * @throws IOException
	 */
	public InternetGatewayDevice[] getGateways(int discoveryTimeout) throws IOException
	{
		if(discoveryTimeout != 0)
		{
			this.discoveryTimeout = discoveryTimeout;
		}

		if(igds == null)
		{
			igds = getGatewaysAgain(discoveryTimeout);
		}

		return igds;
	}

	/**
	 * 重新获取网关信息
	 * @param discoveryTimeout 超时时间，单位：毫秒。如果为0，则使用上次使用的超时时间。
	 * @throws IOException
	 */
	public InternetGatewayDevice[] getGatewaysAgain(int discoveryTimeout) throws IOException
	{
		if(discoveryTimeout != 0)
		{
			this.discoveryTimeout = discoveryTimeout;
		}

		igds = InternetGatewayDevice.getDevices(this.discoveryTimeout);
		return igds;
	}

	/**
	 * 在一个网关设备上添加一条NAT映射信息
	 * @param igd 要添加NAT映射信息的网关设备对象
	 * @param description 相关描述
	 * @param remoteHost 远程主机的IP地址，为null则表示任意主机
	 * @param internalPort 内部端口号，也就是数据将会重定向到的端口
	 * @param externalPort 映射到外部的端口号
	 * @param internalClient 内部主机的IP地址，也就是数据将要重定向到的地址
	 * @param leaseDuration 映射信息有效期，单位：秒。为0则代表长期有效
	 * @param protocol 协议类型，“TCP”或“UDP”
	 * @throws IOException
	 * @throws UPNPResponseException
	 */
	public boolean addNatMapping(InternetGatewayDevice igd, String description, String remoteHost,
							int internalPort, int externalPort, String internalClient,
							int leaseDuration, String protocol) throws IOException, UPNPResponseException
	{
		NatInfo natInfo = getNatInfo(igd, remoteHost, externalPort, protocol);
		if(natInfo != null)
		{
			return false;
		}

		//	执行映射动作并将映射信息记录到缓存中
		boolean flag = igd.addPortMapping(description, remoteHost, internalPort, externalPort, internalClient, leaseDuration, protocol.toUpperCase());
		if(flag)
		{
			natInfo = new NatInfo();
			natInfo.setDescription(description);
			natInfo.setDevice(igd);
			natInfo.setExternalPort(externalPort);
			natInfo.setInternalClient(internalClient);
			natInfo.setInternalPort(internalPort);
			natInfo.setLeaseDuration(leaseDuration);
			natInfo.setProtocol(protocol);
			natInfo.setRemoteHost(remoteHost);
			natInfo.setStartTime(new Date(System.currentTimeMillis()));

			natInfoList.add(natInfo);
		}

		return flag;
	}

	/**
	 * 在所有已知网关设备上添加一条NAT映射信息
	 * @param description 相关描述
	 * @param remoteHost 远程主机的IP地址，为null则表示任意主机
	 * @param internalPort 内部端口号，也就是数据将会重定向到的端口
	 * @param externalPort 映射到外部的端口号
	 * @param internalClient 内部主机的IP地址，也就是数据将要重定向到的地址
	 * @param leaseDuration 映射信息有效期，单位：秒。为0则代表长期有效
	 * @param protocol 协议类型，“TCP”或“UDP”
	 * @throws IOException
	 * @throws UPNPResponseException
	 */
	public void addNatMapping(String description, String remoteHost, int internalPort,
							int externalPort, String internalClient, int leaseDuration, String protocol) throws IOException, UPNPResponseException
	{
		if(igds == null)
		{
			igds = getGatewaysAgain(discoveryTimeout);
		}

		if(igds == null)
		{
			return;
		}

		for(InternetGatewayDevice igd : igds)
		{
			addNatMapping(igd, description, remoteHost, internalPort, externalPort, internalClient, leaseDuration, protocol);
		}
	}

	/**
	 * 删除指定网关设备上的一条NAT映射信息
	 * @param igd 需要删除NAT映射信息的网关设备
	 * @param remoteHost 远程主机的IP地址，为null则表示任意主机
	 * @param externalPort 外部端口
	 * @param protocol 协议类型，“TCP”或“UDP”
	 * @throws IOException
	 * @throws UPNPResponseException
	 */
	public void removeNatMapping(InternetGatewayDevice igd, String remoteHost, int externalPort, String protocol) throws IOException, UPNPResponseException
	{
		int pos = getNatInfoPosition(igd, remoteHost, externalPort, protocol);
		if(pos < 0)
		{
			return;
		}

		NatInfo natInfo = natInfoList.get(pos);
		igd.deletePortMapping(natInfo.getRemoteHost(), externalPort, protocol.toUpperCase());
		natInfoList.remove(pos);
	}

	/**
	 * 删除所有网关设备上的一条NAT映射信息
	 * @param remoteHost 远程主机的IP地址，为null则表示任意主机
	 * @param externalPort 外部端口
	 * @param protocol 协议类型，“TCP”或“UDP”
	 * @throws IOException
	 * @throws UPNPResponseException
	 */
	public void removeNatMapping(String remoteHost, int externalPort, String protocol) throws IOException, UPNPResponseException
	{
		Iterator<NatInfo> it = natInfoList.iterator();
		while(it.hasNext())
		{
			NatInfo natInfo = (NatInfo) it.next();
			InternetGatewayDevice igd = natInfo.getDevice();
			String remote = natInfo.getRemoteHost();
			int extPort = natInfo.getExternalPort();
			String proto = natInfo.getProtocol();
			if(StringTool.sameString(remote, remoteHost) && extPort == externalPort && proto.equalsIgnoreCase(protocol))
			{
				igd.deletePortMapping(natInfo.getRemoteHost(), externalPort, protocol.toUpperCase());
				it.remove();
			}
		}
	}

	//	在当前NAT映射表里查询映射信息
	private NatInfo getNatInfo(InternetGatewayDevice igd, String remoteHost, int externalPort, String protocol) throws UPNPResponseException, IOException
	{
		for(NatInfo natInfo : natInfoList)
		{
			String ip = natInfo.getDevice().getExternalIPAddress();
			String remote = natInfo.getRemoteHost();
			int extPort = natInfo.getExternalPort();
			String proto = natInfo.getProtocol();
			if(ip.equals(igd.getExternalIPAddress()) && StringTool.sameString(remote, remoteHost) &&  extPort == externalPort && proto.equalsIgnoreCase(protocol))
			{
				return natInfo;
			}
		}

		return null;
	}

	//	在当前NAT映射表里查询映射信息所在的位置索引
	private int getNatInfoPosition(InternetGatewayDevice igd, String remoteHost, int externalPort, String protocol) throws UPNPResponseException, IOException
	{
		int pos = 0;
		for(NatInfo natInfo : natInfoList)
		{
			String ip = natInfo.getDevice().getExternalIPAddress();
			String remote = natInfo.getRemoteHost();
			int extPort = natInfo.getExternalPort();
			String proto = natInfo.getProtocol();
			if(ip.equals(igd.getExternalIPAddress()) && StringTool.sameString(remote, remoteHost) &&  extPort == externalPort && proto.equalsIgnoreCase(protocol))
			{
				return pos;
			}

			++ pos;
		}

		return -1;
	}
}
